package org.pwwong.fxscalautils.components

import java.awt.event.{MouseEvent, MouseWheelEvent}

import com.sun.scenario.scenegraph.fx.FXNode
import com.sun.scenario.scenegraph.event.SGMouseListener
import com.sun.scenario.scenegraph.SGNode

import org.pwwong.fxscalautils.Predef._
import org.pwwong.fxscalautils.components.StateMachine.State
import org.pwwong.fxscalautils.components.containers._
import org.pwwong.fxscalautils.components.layouthints._

import org.pwwong.scalautils.animation._
import org.pwwong.scalautils.animation.Predef._
import org.pwwong.scalautils.property._
import org.pwwong.scalautils.event.{Event}
import org.pwwong.scalautils.signals.{BindableSignal,ConstantSignal,Signal}
import org.pwwong.scalautils.signals.Predef._
import org.pwwong.scalautils.time._

import scala.collection.mutable
import scala.collection

object Component {  
  // Property
  protected[Component] abstract class Property[T](_boundTo:Signal[_<:T]) extends Prop[T](_boundTo) {
    type ComponentType <: Component
    protected val component:ComponentType
    private var defaultValue = _boundTo
    private val stateValues = mutable.Map[State,Signal[_<:T]]()
    
    def apply(builder:EndAnimation[T]):ComponentType = {
      val rtn = new AnimatedSignal[T](
        apply(), 
        builder.endValue, 
        builder.repeatBehavior, 
        builder.duration.get,
        Milliseconds(1000/builder.framesPerSec),
        builder.evaluator,
        builder.interpolator
      )
      rtn.start
      apply(rtn)
    }
    
    def apply[U<:T](newVal:U):ComponentType = { apply(new ConstantSignal[T](newVal.asInstanceOf[T])); component }
    def apply[U<:T](bindSig:Signal[U]):ComponentType = { defaultValue = bindSig; this <= bindSig; component }
    
    def apply[U<:T](stateSymbol:Symbol,newVal:U):ComponentType = apply[U](stateSymbol,new ConstantSignal[U](newVal))
    def apply[U<:T](stateSymbol:Symbol,bindSig:Signal[U]):ComponentType = {
      val state = component._stateMachine.getState(stateSymbol)
      if( !stateValues.contains(state) ){
        stateValues += (state -> bindSig)
        state.onEnter(this){ prevCur => 
          Property.this <= stateValues(prevCur._2) 
        }
      }else{
        stateValues(state) = bindSig
      }
      component
    }
    
    // Constructor
    component.defaultState.onEnter(this){ prevCur => 
      Property.this <= defaultValue 
    }
  }
  
  /**
   * Specialized Property for SizeValues (width, height).
   * A specialized subclass of Property was needed to handle Animations.  
   * As SizeValues can be expressed Absolutely (px) or Relatively (%%), Animations from an Absolute to Relative SizeValue (or vice-versa) require knowledge
   * of the Component (the Component's parent's size).  
   */
  protected[Component] abstract class SizeProperty(bt:Signal[SizeValue], private val _parentDimFunc:Container=>Signal[Double]) extends Property[SizeValue](bt) {
    var deferredAnimation:StartEndAnimation[_<:SizeValue] = null
    private def deferredAnimationStarter(ev:Any) {
      //TODO
      //component.parent.onChange.removeListener(this)
      //component.parent().get.curWidth.onChange.removeListener(this)
      if(component.parent().isDefined){
        if(_parentDimFunc(component.parent().get)() > 0.0 && deferredAnimation != null)
          applyStartEndAnimation(deferredAnimation)
        else	
            _parentDimFunc(component.parent().get).onChange(this)(deferredAnimationStarter)
      }
    }
    private def applyStartEndAnimation[U<:SizeValue](builder:StartEndAnimation[U]):ComponentType = {
      val rtn = new AnimatedSignal[U](
        builder.endValue match { 
          case PX(_) => convertToPX(builder.startValue).asInstanceOf[U]
          case PCT(_) => convertToPCT(builder.startValue).asInstanceOf[U]
        },
        builder.endValue, 
        builder.repeatBehavior, 
        builder.duration.get,
        Milliseconds(1000/builder.framesPerSec),
        builder.endValue match {
          case PX(_) => PXEval.asInstanceOf[(Double,U,U)=>U]
          case PCT(_) => PCTEval.asInstanceOf[(Double,U,U)=>U]
        },
        builder.interpolator
      )
      rtn.start
      deferredAnimation = null
      apply(rtn)
    }
    def apply[U<:SizeValue](builder:StartEndAnimation[U]):ComponentType = {
      if(component.parent().isDefined){
        applyStartEndAnimation(builder)
      }else{
        deferredAnimation = builder
        component.parent.onChange(this)(deferredAnimationStarter)
        component
      }
    }
    
    def toPX:PX = convertToPX(apply())
    def convertToPX(src:SizeValue):PX = src match {
      case PX(_)     => src.asInstanceOf[PX]
      case PCT(pctv) => require(component.parent().isDefined)
                        _parentDimFunc(component.parent().get)()*(pctv.value/100.0) px 
    }
    
    def toPCT:PCT = convertToPCT(apply())
    def convertToPCT(src:SizeValue):PCT = src match {
      case PX(pxv)   => require(component.parent().isDefined)
                        100.0*(pxv.value.asInstanceOf[Double]/_parentDimFunc(component.parent().get)()) %%
      case PCT(_)    => src.asInstanceOf[PCT]
    }
  }
  
  
  protected[Component] abstract class XPositionProperty(bt:Signal[XPosition[PositionValue]]) extends Property[XPosition[PositionValue]](bt) {
    /**
     * A StartEndAnimation that cannot be used to create an AnimatedSignal.
     * Reasons for this could be because Component's Parent is defined and:
     *   1) Animation starts and ends with different Origins
     *   2) Animation starts and ends with different Offset units (PX,%%)
     */
    private var deferredAnimation:StartEndAnimation[_<:XPosition[PositionValue]] = null
    
    /**
     * Listener for when an AnimatedSignal can be created from a Deferred Animation.
     */
    private def deferredAnimationStarter(ev:Any) {
      //TODO
      //component.parent.onChange.removeListener(this)
      //component.parent().get.curWidth.onChange.removeListener(this)
      if(component.parent().isDefined)
        if(component.parent().get.curWidth() > 0.0 && deferredAnimation != null)
          applyStartEndAnimation(deferredAnimation)
      else	
        component.parent().get.curWidth.onChange(this)( deferredAnimationStarter ) 
    }
    
    /**
     * Creates and binds AnimatedSignal from StartEndAnimation.
     * Clears deferredAnimation.
     */
    private def applyStartEndAnimation[U<:XPosition[PositionValue]](builder:StartEndAnimation[U]):ComponentType = {
      val rtn = new AnimatedSignal[U](
        builder.endValue.offset match { 
          case PX(_) => convertToPX(builder.startValue, builder.endValue.origin).asInstanceOf[U]
          case PCT(_) => convertToPCT(builder.startValue, builder.endValue.origin).asInstanceOf[U]
        },
        builder.endValue, 
        builder.repeatBehavior, 
        builder.duration.get,
        Milliseconds(1000/builder.framesPerSec),
        (builder.endValue.offset match { 
          case PX(_) => XPositionPXEval
          case PCT(_) => XPositionPCTEval
        }).asInstanceOf[(Double,U,U)=>U],
        builder.interpolator
      )
      rtn.start
      deferredAnimation = null
      apply(rtn)
    }
    /**
     * Handles StartEndAnimation assignment.
     * If owning Component's parent is defined, immediately create and bind AnimatedSignal from StartEndAnimation.
     * Otherwise defer creation and binding until parent is defined and has been laid out.
     */
    def apply[U<:XPosition[PositionValue]](builder:StartEndAnimation[U]):ComponentType = {
      if(component.parent().isDefined){
        applyStartEndAnimation(builder)
      }else{
        component.parent.onChange(this)( deferredAnimationStarter )
        deferredAnimation = builder
        component
      }
    }
    
    def toPX:XPosition[PX] = convertToPX(apply())
    private def convertToPX(src:XPosition[_]):XPosition[PX] = src.offset match {
      case PX(_)     => src.asInstanceOf[XPosition[PX]]
      case pct:PCT => require(component.parent().isDefined)
                      XPosition[PX](src.origin, component.w.convertToPX(pct) )
                      //XPosition[PX](src.origin, component.parent().get.curWidth()*pctv.value/100.0 px ) 
    }
    
    def toPCT:XPosition[PCT] = convertToPCT(apply())
    private def convertToPCT(src:XPosition[_]):XPosition[PCT] = src.offset match {
      case px:PX   => require(component.parent().isDefined)
                      XPosition[PCT](src.origin, component.w.convertToPCT(px))
      case PCT(_)    => src.asInstanceOf[XPosition[PCT]]
    }
    
    
    def toPX(destOrigin:XOrigins.Value):XPosition[PX] = convertToPX(apply(), destOrigin) 
    private def convertToPX(src:XPosition[_],destOrigin:XOrigins.Value):XPosition[PX] = {
      if(src.origin == destOrigin) convertToPX(src)
      else {
        require( component.parent().isDefined )
        src match {
          case XPosition(origin, _) => origin match {
            case LeftOrigin => destOrigin match {
              case CenterOrigin => XPosition(CenterOrigin, (convertToPX(src).offset.value - component.parent().get.curWidth()/2.0) px) 
              case RightOrigin => XPosition(RightOrigin, (convertToPX(src).offset.value - component.parent().get.curWidth()) px)
            }
            case CenterOrigin => destOrigin match {
              case LeftOrigin => XPosition(LeftOrigin, (convertToPX(src).offset.value + component.parent().get.curWidth()/2.0) px) 
              case RightOrigin => XPosition(RightOrigin, (convertToPX(src).offset.value - component.parent().get.curWidth()/2.0) px)
            } 
            case RightOrigin => destOrigin match {
              case LeftOrigin =>  XPosition(LeftOrigin, (convertToPX(src).offset.value + component.parent().get.curWidth()) px)
              case CenterOrigin => XPosition(CenterOrigin, (convertToPX(src).offset.value + component.parent().get.curWidth()/2.0) px)
            }
          } 
        }
      }
    }
    
    /**
     * Convert an XPosition's Offset to PX with respect to an Origin and this Property's Component  
     */
    final def convertToPCT(src:XPosition[_],destOrigin:XOrigins.Value):XPosition[PCT] = {
      // If Origins are the same, just convert to offset PX
      if(src.origin == destOrigin) convertToPCT(src)
      // Otherwise, transform to destination Origin
      else {
        require( (component.parent().isDefined) )  // Transforming between Origins always requires knowledge about Component's Parent's Size
        src match {
          case XPosition(origin, _) => origin match {
            case LeftOrigin => destOrigin match {
              case CenterOrigin => XPosition(CenterOrigin, (convertToPCT(src).offset.value - 50.0) %%) 
              case RightOrigin => XPosition(RightOrigin, (convertToPCT(src).offset.value - 100.0) %%)
            }
            case CenterOrigin => destOrigin match {
              case LeftOrigin => XPosition(LeftOrigin, (convertToPCT(src).offset.value + 50.0) %%) 
              case RightOrigin => XPosition(RightOrigin, (convertToPCT(src).offset.value - 50.0) %%)
            } 
            case RightOrigin => destOrigin match {
              case LeftOrigin =>  XPosition(LeftOrigin, (convertToPCT(src).offset.value + 100.0) %%)
              case CenterOrigin => XPosition(CenterOrigin, (convertToPCT(src).offset.value + 50.0) %%)
            }
          } 
        }
      }
    }
  }
  
  
  protected[Component] abstract class YPositionProperty(bt:Signal[YPosition[PositionValue]]) extends Property[YPosition[PositionValue]](bt) {
    var deferredAnimation:StartEndAnimation[_<:YPosition[PositionValue]] = null
    private def deferredAnimationStarter(ev:Any) {
      //TODO
      //component.parent.onChange.removeListener(this)
      //component.parent().get.curWidth.onChange.removeListener(this)
      if(component.parent().isDefined)
        if(component.parent().get.curHeight() > 0.0 && deferredAnimation != null)
          applyStartEndAnimation(deferredAnimation)
      else	
        component.parent().get.curHeight.onChange(this)( deferredAnimationStarter ) 
    }
    private def applyStartEndAnimation[U<:YPosition[PositionValue]](builder:StartEndAnimation[U]):ComponentType = {
      val rtn = new AnimatedSignal[U](
        builder.endValue.offset match { 
          case PX(_) => convertToPX(builder.startValue, builder.endValue.origin).asInstanceOf[U]
          case PCT(_) => convertToPCT(builder.startValue, builder.endValue.origin).asInstanceOf[U]
        },
        builder.endValue, 
        builder.repeatBehavior, 
        builder.duration.get,
        Milliseconds(1000/builder.framesPerSec),
        (builder.endValue.offset match { 
          case PX(_) => YPositionPXEval
          case PCT(_) => YPositionPCTEval
        }).asInstanceOf[(Double,U,U)=>U],
        builder.interpolator
      )
      rtn.start
      deferredAnimation = null
      apply(rtn)
    }
    def apply[U<:YPosition[PositionValue]](builder:StartEndAnimation[U]):ComponentType = {
      if(component.parent().isDefined){
        applyStartEndAnimation(builder)
      }else{
        component.parent.onChange(this)( deferredAnimationStarter )
        deferredAnimation = builder
        component
      }
    }
    def toPX:YPosition[PX] = convertToPX(apply())
    private def convertToPX(src:YPosition[_]):YPosition[PX] = src.offset match {
      case PX(_)     => src.asInstanceOf[YPosition[PX]]
      case pct:PCT => require(component.parent().isDefined)
                      YPosition[PX]( src.origin, component.h.convertToPX(pct) ) 
    }
    def toPCT:YPosition[PCT] = convertToPCT(apply()) 
    private def convertToPCT(src:YPosition[_]):YPosition[PCT] = src.offset match {
      case px:PX  => require(component.parent().isDefined)
                     YPosition[PCT]( src.origin, component.h.convertToPCT(px) )
      case PCT(_) => src.asInstanceOf[YPosition[PCT]]
    }
    
    def toPX(destOrigin:YOrigins.Value):YPosition[PX] = convertToPX(apply(), destOrigin) 
    private def convertToPX(src:YPosition[_],destOrigin:YOrigins.Value):YPosition[PX] = {
      if(src.origin == destOrigin) convertToPX(src)
      else {
        require( component.parent().isDefined )
        src match {
          case YPosition(origin, _) => origin match {
            case TopOrigin => destOrigin match {
              case MiddleOrigin => YPosition(MiddleOrigin, (convertToPX(src).offset.value - component.parent().get.curHeight()/2.0) px) 
              case BottomOrigin => YPosition(BottomOrigin, (convertToPX(src).offset.value - component.parent().get.curHeight()) px)
            }
            case MiddleOrigin => destOrigin match {
              case TopOrigin => YPosition(TopOrigin, (convertToPX(src).offset.value + component.parent().get.curHeight()/2.0) px) 
              case BottomOrigin => YPosition(BottomOrigin, (convertToPX(src).offset.value - component.parent().get.curHeight()/2.0) px)
            } 
            case BottomOrigin => destOrigin match {
              case TopOrigin =>  YPosition(TopOrigin, (convertToPX(src).offset.value + component.parent().get.curHeight()) px)
              case MiddleOrigin => YPosition(MiddleOrigin, (convertToPX(src).offset.value + component.parent().get.curHeight()/2.0) px)
            }
          } 
        }
      }
    }
    
    def toPCT(destOrigin:YOrigins.Value):YPosition[PCT] = convertToPCT(apply(), destOrigin)
    private def convertToPCT(src:YPosition[_],destOrigin:YOrigins.Value):YPosition[PCT] = {
      if(src == destOrigin) convertToPCT(src)
      else {
        require( (component.parent().isDefined) )
        src match {
          case YPosition(origin, _) => origin match {
            case TopOrigin => destOrigin match {
              case MiddleOrigin => YPosition(MiddleOrigin, (convertToPCT(src).offset.value - 50.0) %%) 
              case BottomOrigin => YPosition(BottomOrigin, (convertToPCT(src).offset.value - 100.0) %%)
            }
            case MiddleOrigin => destOrigin match {
              case TopOrigin => YPosition(TopOrigin, (convertToPCT(src).offset.value + 50.0) %%) 
              case BottomOrigin => YPosition(BottomOrigin, (convertToPCT(src).offset.value - 50.0) %%)
            } 
            case BottomOrigin => destOrigin match {
              case TopOrigin => YPosition(TopOrigin, (convertToPCT(src).offset.value + 100.0) %%)
              case MiddleOrigin => YPosition(MiddleOrigin, (convertToPCT(src).offset.value + 50.0) %%)
            }
          } 
        }
      }
    }
  }
  
  // BoundedProperty
  protected[Component] abstract class BoundedProperty[T](protected val boundedToProp:Property[T]) extends Property[T](boundedToProp) { self:BoundedProperty[T] =>
    override def <=[U<:T](other:Signal[U]):self.type = {
      boundedToProp <= other
      this
    }
  }
}

abstract class Component() { compSelf:Component =>
  
  /**
   * Convenience method for declaring a new Property owned by this Component 
   */
  protected def Property[T](boundTo:T) = new Component.Property[T](new ConstantSignal[T](boundTo)){
    type ComponentType = compSelf.type
    override protected val component:compSelf.type = compSelf
  }
  
  /**
   * Convenience method for declaring a new Property owned by this Component 
   */
  protected def Property[T](boundTo:Signal[T]) = new Component.Property[T](boundTo){
    type ComponentType = compSelf.type
    override protected val component:compSelf.type = compSelf
  }
  protected def BoundedProperty[T](boundTo:Component.Property[T]) = new Component.BoundedProperty[T](boundTo){
    type ComponentType = compSelf.type
    override protected val component:compSelf.type = compSelf 
  }
    
  // FX (Project Scene Graph backing)
  type FXImpl <: FXNode
  final private[this] var _compNodeImpl:FXImpl = null.asInstanceOf[FXImpl]
  final def compNodeImpl:FXImpl = _compNodeImpl
  protected def compNodeImpl_=(newImpl:FXImpl) = {
    require(null == _compNodeImpl, "compNodeImpl already set")
    _compNodeImpl = newImpl
    compNodeImpl.addMouseListener(NFMouseListener)
  }
  
  // States
  private val _stateMachine = new StateMachine
  def CreateDefaultState(defaultStateSymbol:Symbol):State = _stateMachine.setDefaultState(defaultStateSymbol)
  def CreateState(newStateSymbol:Symbol):State = {
    require( _stateMachine.isDefaultSet , compSelf+" needs to create a default state before adding other states ("+newStateSymbol+")" )
    _stateMachine.addState(newStateSymbol)
  }
  val defaultState = _stateMachine.defaultState
  
  // Hierarchy 
  final protected[components] val _parent = Prop[Option[Container]](None)
  final val parent:Signal[Option[Container]] = _parent
  
  // Layout hints (Client recommendations)
  val x = new Component.XPositionProperty(new ConstantSignal(Left)){
    type ComponentType = compSelf.type
    override protected val component:compSelf.type = compSelf
  }
  val y = new Component.YPositionProperty(new ConstantSignal(Top)){
    type ComponentType = compSelf.type
    override protected val component:compSelf.type = compSelf
  }
  def center = { anchorX(Center); x }
  def left = { anchorX(Left); x }
  def right = { anchorX(Right); x }
  
  def top = { anchorY(Top); y }
  def bottom = { anchorY(Bottom); y }
  def middle = { anchorY(Middle); y }
  
  val anchorX = Property[XPosition[_<:PositionValue]](Left)
  val anchorY = Property[YPosition[_<:PositionValue]](Top)
  def anchor(newx:XPosition[_<:PositionValue], newy:YPosition[_<:PositionValue]):compSelf.type = {anchorX(newx); anchorY(newy); compSelf}
  def anchor(xsig:Signal[XPosition[_<:PositionValue]],ysig:Signal[YPosition[_<:PositionValue]]):compSelf.type = {anchorX(xsig); anchorY(ysig); compSelf}
  
  val rotation = Property[Double](0.0)
    
  // Size Hint (Component recommendations)
  val preferredWidth:Signal[Double]
  val preferredHeight:Signal[Double]
  
  // Position/Size (Actual)
  protected[components] val _x,_y,_width,_height = BindableSignal[Double](0.0)  
  final val curX:Signal[Double] = _x
  final val curY:Signal[Double] = _y
  final val curWidth:Signal[Double] = _width
  final val curHeight:Signal[Double] = _height
  
  final val w = new Component.SizeProperty(new ConstantSignal[SizeValue](preferred), _.curWidth){
    type ComponentType = compSelf.type
    override protected val component:compSelf.type = compSelf
  }
  final val h = new Component.SizeProperty(new ConstantSignal[SizeValue](preferred), _.curHeight){
    type ComponentType = compSelf.type
    override protected val component:compSelf.type = compSelf
  }
  
  // Mouse Interactions
  final val onMouseEntered = Event[(MouseEvent,Component)]
  final val onMouseMoved = Event[(MouseEvent,Component)]
  final val onMousePressed = Event[(MouseEvent,Component)]
  final val onMouseReleased = Event[(MouseEvent,Component)]
  final val onMouseDragged = Event[(MouseEvent,Component)]
  final val onMouseClicked = Event[(MouseEvent,Component)]
  final val onMouseExited = Event[(MouseEvent,Component)]
  final val onMouseWheelMoved = Event[(MouseWheelEvent,Component)]

  private object NFMouseListener extends SGMouseListener {
    def mouseClicked(e:MouseEvent, node:SGNode) = onMouseClicked fire (e,compSelf)
  	def mouseDragged(e:MouseEvent, node:SGNode) = onMouseDragged fire (e,compSelf)
  	def mouseEntered(e:MouseEvent, node:SGNode) = onMouseEntered fire (e,compSelf)
  	def mouseExited(e:MouseEvent, node:SGNode) = onMouseExited fire (e,compSelf)
  	def mouseMoved(e:MouseEvent, node:SGNode) = onMouseMoved fire (e,compSelf)
  	def mousePressed(e:MouseEvent, node:SGNode) = onMousePressed fire (e,compSelf)
  	def mouseReleased(e:MouseEvent, node:SGNode) = onMouseReleased fire (e,compSelf)
  	def mouseWheelMoved(e:MouseWheelEvent, node:SGNode) = onMouseWheelMoved fire (e,compSelf)
  }
  
  // Reactions
  final protected def resetAnchorX {
    compNodeImpl.setAnchorX{
      (anchorX().origin match {
        case LeftOrigin => 0.0
        case RightOrigin => curWidth()
        case CenterOrigin => curWidth()/2.0
      }) + 
      (anchorX().offset match {
        case PX(pxv) => pxv.toDouble
        case PCT(pctv) => curWidth()*(pctv/100.0)
      })
    }
  }
  final protected def resetAnchorY {
    compNodeImpl.setAnchorY{
      (anchorY().origin match {
        case TopOrigin => 0.0
        case MiddleOrigin => curHeight()/2.0
        case BottomOrigin => curHeight()
      }) + 
      (anchorY().offset match {
        case PX(pxv) => pxv.toDouble
        case PCT(pctv) => curHeight()*(pctv/100.0)
      })
    }
  }
  final protected def resetX = compNodeImpl.setTranslateX{ curX()-compNodeImpl.getAnchorX() }
  final protected def resetY = compNodeImpl.setTranslateY( curY()-compNodeImpl.getAnchorY() )
  
  onAnyChanges(anchorX,curWidth)(this){ resetAnchorX }
  onAnyChanges(anchorY,curHeight)(this){ resetAnchorY }
  onAnyChanges(curX,curWidth,anchorX)(this){ resetX }
  onAnyChanges(curY,curHeight,anchorY)(this){ resetY }
  rotation.onChange(this){ compNodeImpl.setRotation(_) }
}
